package odm;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public final class SocketChannelUtils {
	public static int TIMEOUT_DEFAULT = 60 * 1000;

	public static void read(SocketChannel src, ByteBuffer dest)
			throws SocketTimeoutException, IOException {
		if (dest != null && dest.hasRemaining() && src != null && src.isOpen()) {
			boolean blocking = src.isBlocking();
			if (blocking) {
				src.configureBlocking(false);
			}

			try {
				int timeout = src.socket().getSoTimeout();
				if (timeout <= 0) {
					timeout = TIMEOUT_DEFAULT;
				}
				int lengthRead = 0;
				long clock = 0L;
				while (dest.hasRemaining()) {
					lengthRead = src.read(dest);
					if (lengthRead == -1) {
						break;
					} else if (lengthRead == 0) {
						if (clock == 0L) {
							clock = System.currentTimeMillis();
						} else if ((System.currentTimeMillis() - clock) >= timeout) {
							throw new SocketTimeoutException("socket read to buffer timeout");
						}
					} else if (lengthRead > 0) {
						if (clock > 0L) {
							clock = 0L;
						}
					}
				}
			} finally {
				if (blocking) {
					src.configureBlocking(blocking);
				}
			}
		}
	}

	public static void read(SocketChannel src, long length, FileChannel dest, ByteBuffer buff)
			throws SocketTimeoutException, IOException {
		if (src != null && src.isOpen() && dest != null && dest.isOpen()
				&& length > 0) {
			boolean blocking = src.isBlocking();
			if (blocking) {
				src.configureBlocking(false);
			}
			
			try {
				int timeout = src.socket().getSoTimeout();
				if (timeout <= 0) {
					timeout = TIMEOUT_DEFAULT;
				}
				
				long remaining = length;
				int lengthRead = 0;
				long clock = 0L;
				ByteBuffer buffer = buff;
				if(buffer == null) {
					buffer = ByteBuffer.allocate(65536); 
				} else {
					buffer.clear();
				}
				if (remaining < buffer.capacity()) {
					buff.limit((int) remaining);
				}
				while (remaining > 0) {
					lengthRead = src.read(buffer);
					if (lengthRead == -1) {
						throw new FileException("session closed by server prematurely");
					} else if (lengthRead == 0) {
						if (clock == 0L) {
							clock = System.currentTimeMillis();
						} else if ((System.currentTimeMillis() - clock) >= timeout) {
							throw new SocketTimeoutException("socket read to file timeout");
						}
					} else if (lengthRead > 0) {
						if (clock > 0L) {
							clock = 0L;
						}
						buffer.flip();
						dest.write(buffer);
						buffer.clear();
						remaining -= lengthRead;
						if (remaining < buffer.capacity()) {
							buffer.limit((int) remaining);
						}
					}
				}
				dest.force(true);
			} finally {
				if (blocking) {
					src.configureBlocking(blocking);
				}
			}
		}
	}

	public static void write(SocketChannel dest, ByteBuffer src)
			throws SocketTimeoutException, IOException {
		if (src != null && src.hasRemaining() && dest != null && dest.isOpen()) {
			boolean blocking = dest.isBlocking();
			if (blocking) {
				dest.configureBlocking(false);
			}
			
			try {
				int timeout = dest.socket().getSoTimeout();
				if (timeout <= 0) {
					timeout = TIMEOUT_DEFAULT;
				}
				
				int lengthWritten = 0;
				long clock = 0L;
				while (src.hasRemaining()) {
					lengthWritten = dest.write(src);
					if (lengthWritten == 0) {
						if (clock == 0L) {
							clock = System.currentTimeMillis();
						} else if ((System.currentTimeMillis() - clock) >= timeout) {
							throw new SocketTimeoutException("socket write from buffer timeout");
						}
					} else if (lengthWritten > 0) {
						if (clock > 0L) {
							clock = 0L;
						}
					}
				}
			} finally {
				if (blocking) {
					dest.configureBlocking(blocking);
				}
			}
		}
	}

	public static void write(SocketChannel dest, long length, FileChannel src, ByteBuffer buff)
			throws SocketTimeoutException, IOException {
		if(dest != null && dest.isOpen() && src != null && src.isOpen()) {
			boolean block = dest.isBlocking();
			if(block) {
				dest.configureBlocking(false);
			}
			
			try {
				int timeout = dest.socket().getSoTimeout();
				if (timeout <= 0) {
					timeout = TIMEOUT_DEFAULT;
				}
				
				long remaining = length;
				int lengthRead = 0;
				int lengthWritten = 0;
				long fileClock = 0L;
				long socketClock = 0L;
				ByteBuffer buffer = buff;
				if(buffer == null) {
					buffer = ByteBuffer.allocate(65536); 
				} else {
					buffer.clear();
				}
				if (remaining < buffer.capacity()) {
					buff.limit((int) remaining);
				}
				while (remaining > 0) {
					lengthRead = src.read(buffer);
					if (lengthRead == -1) {
						throw new FileException("end of file stream reached prematurely");
					} else if (lengthRead == 0) {
						if (fileClock == 0L) {
							fileClock = System.currentTimeMillis();
						} else if ((System.currentTimeMillis() - fileClock) >= timeout) {
							throw new SocketTimeoutException("file read to socket timeout");
						}
					} else if (lengthRead > 0) {
						if (fileClock > 0L) {
							fileClock = 0L;
						}
						buffer.flip();
						while (buffer.hasRemaining()) {
							lengthWritten = dest.write(buffer);
							if (lengthWritten == 0) {
								if (socketClock == 0L) {
									socketClock = System.currentTimeMillis();
								} else if ((System.currentTimeMillis() - socketClock) >= timeout) {
									throw new SocketTimeoutException("socket write from file timeout");
								}
							} else if (lengthWritten > 0) {
								if(socketClock > 0L) {
									socketClock = 0L;
								}
							}	
						}
						buffer.clear();
						remaining -= lengthRead;
						if (remaining < buffer.capacity()) {
							buff.limit((int) remaining);
						}
					}
				}
			} finally {
				if(block) {
					dest.configureBlocking(block);
				}
			}
		}
	}
}
